{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## <center>Открытый курс по машинному обучению\n",
    "Автор материала: Data Science интерн Ciklum, студент магистерской программы CSDS UCU Виталий Радченко. Материал распространяется на условиях лицензии [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#  Мастер-класс. Deep Learning в решении задач сентимент-анализа"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "10 июня 2017 г. Митап победителей открытого курса в Московском офисе Mail.ru Group. [Видео-трансляция](https://www.youtube.com/watch?v=dQAgJ1J6hHk)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Постановка задачи\n",
    "Для многих компаний важно знать, что о них пишут в интернете. Поэтому одна из их основных задач – классификация отзывов в интернете на позитивные и негативные, на языке машинного обучения это будет означать решение задачи бинарной классификации.\n",
    "\n",
    "## Данные\n",
    "\n",
    "Данные могут быть взяты из нескольких источников:\n",
    "- размеченные вручную отзывы о вашей компании;\n",
    "- открытые датасеты (Amazon, IMDb, RT, Twitter и др.);\n",
    "- данные, полученные с помощью парсинга сайтов с отзывами (iHerb, iTunes, GoodReads, Expedia, Yelp и др.). Но в данном случае важно помнить про \"Условия пользования сайтом\".\n",
    "\n",
    "На этом семинаре мы будем использовать данные IMDb и RT, в которых оценки 1-2 – негативные отзывы (0), а оценки 4-5 – позитивные(1).\n",
    "\n",
    "## Подходы к решению\n",
    "\n",
    "Поскольку эта простая задача бинарной классификации, ее можно решать массой разных способов:\n",
    "- линейные модели ( логистическая регрессия, SVM, Naive Bayes и др.);\n",
    "- деревья, ансамбли, бустинг (дерево решений, случайный лес, Xgboost и др.);\n",
    "- Библиотека Facebook [FastText](https://github.com/facebookresearch/fastText);\n",
    "- нейронные сети на словах и символах (рекуррентные, LSTM, GRU, CNN и др.).\n",
    "\n",
    "Мы рассмотрим только решение, основанное на нейронных сетях, а туториалы по остальным подходам вы можете найти в [данном](https://github.com/udsclub/workshop) репозитории, [видео](https://www.youtube.com/watch?v=Zq-cw7YyHKA) также прилагается. \n",
    "\n",
    "## Загрузка данных\n",
    "Скачать можно [отсюда](https://yadi.sk/d/57oSkIZc3JxtY3)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "import numpy as np\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "# инициализируем пути к файлам\n",
    "BASE_DIR = '../'\n",
    "TEXT_DATA_DIR = BASE_DIR + 'data/'\n",
    "TEXT_DATA_FILE = \"movie_reviews.csv\"\n",
    "HEADER = True\n",
    "\n",
    "# инициализируем параметры \n",
    "VALIDATION_SPLIT = 0.1\n",
    "RANDOM_SEED = 42\n",
    "\n",
    "\n",
    "def load_data():\n",
    "    # функция для загрузки данных\n",
    "    x = []\n",
    "    y = []\n",
    "    with open(os.path.join(TEXT_DATA_DIR, TEXT_DATA_FILE), \"r\", encoding=\"utf-8\") as f:\n",
    "        if HEADER:\n",
    "            _ = next(f)\n",
    "        for line in f:\n",
    "            temp_y, temp_x = line.rstrip(\"\\n\").split(\",\", 1)\n",
    "            x.append(temp_x)\n",
    "            y.append(temp_y)\n",
    "    return x, y\n",
    "\n",
    "data, labels = load_data()\n",
    "labels = np.asarray(labels, dtype='int8')\n",
    "\n",
    "# разделение выборки на тренировочную и тестовую\n",
    "data_train, data_test, labels_train, labels_test = \\\n",
    "    train_test_split(data, np.asarray(labels, dtype='int8'),\n",
    "                     test_size=VALIDATION_SPLIT, random_state=RANDOM_SEED, stratify=labels)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Предобработка данных\n",
    "\n",
    "В \"сыром\" виде нельзя подавать данные в модель, их нужно правильно преобразовать. Для начала рассмотрим нейронные сети на словах. \n",
    "\n",
    "Давайте представим каждое слово как вектор чисел, и чем меньше векторное расстояние между данными словами, тем сильнее они похожи. Для такого представления существует несколько подходов, но самые известные из них – это **word2vec** и **glove**.\n",
    "\n",
    "### Word2Vec\n",
    "\n",
    "Word2Vec – это простая нейронная сеть с одним скрытым слоем, которая имеет две спецификации:\n",
    "- Cbow – на основе окружающих слов предсказываем центральное слово \n",
    "- SkipGram – на основе центрального слова предсказываем окружающие\n",
    "<img src=\"https://silvrback.s3.amazonaws.com/uploads/60a81cd5-5189-4550-9709-523b3feef3d1/sentiment_01_large.png\" alt=\"word2vec\" style=\"width: 500px;\"/>\n",
    "\n",
    "Источник: [Efficient Estimation of Word Representations in Vector Space](https://arxiv.org/pdf/1301.3781.pdf)\n",
    "\n",
    "\n",
    "### GloVe\n",
    "\n",
    "В отличие от word2vec, GloVe – это алгоритм обучения \"без учителя\" для получения векторных представлений для слов. Обучение проводится по совокупной статистике матрицы совпадений по словам, а полученные представления показывают интересные линейные подструктуры векторного пространства слов.\n",
    "<img src=\"http://building-babylon.net/wp-content/uploads/2016/02/glove-matrix-factorisation-5.jpg\" alt=\"glove\" style=\"width: 500px;\"/>\n",
    "\n",
    "Источник: [GloVe: Global Vectors for Word Representations](http://building-babylon.net/2015/07/29/glove-global-vectors-for-word-representations/)\n",
    "\n",
    "Word2Vec и GloVe изучают геометрические кодировки (векторы) слов из их информации о совпадении (как часто они появляются вместе в больших текстовых корпусах). Они отличаются тем, что word2vec является «предиктивной» моделью, тогда как GloVe является моделью на основе «подсчета статистик».\n",
    "\n",
    "Пример векторной репрезентации:\n",
    "<img src=\"http://blog.yhat.com/static/img/words2map-2.png\" alt=\"glove\" style=\"width: 400px;\"/>\n",
    "\n",
    "Источник: [Making Sense of Everything with words2map](http://blog.yhat.com/posts/words2map.html)\n",
    "Данные векторные репрезентации принято называть эмбеддингами. Эмбеддинги можно натренировать самостоятельно на вашем корпусе текстов (например, с помощью библотеки gensim), а также можно взять уже [предобученые модели](https://github.com/3Top/word2vec-api). \n",
    "\n",
    "В этом воркшопе мы будем использовать предобученые вектора, так как наш корпус текста не достаточно большой для тренировки своей модели. Для скорости вычислений воспользуемся векторами [glove размерностью 50](http://nlp.stanford.edu/data/glove.6B.zip).\n",
    "\n",
    "Преобразуем слова в векторы."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.preprocessing.sequence import pad_sequences\n",
    "from keras.preprocessing.text import Tokenizer\n",
    "\n",
    "# инициализируем параметры словаря и эмбеддингов\n",
    "MAX_NB_WORDS = 10000\n",
    "MAX_SEQUENCE_LENGTH = 40\n",
    "\n",
    "print(\"Предложение до предобработки:\\n\", data_train[0])\n",
    "\n",
    "# с помощью Tokenizer создаем словарь \n",
    "tokenizer = Tokenizer(num_words=MAX_NB_WORDS, filters='#$%&()*+-/:;<=>@[\\\\]^{|}~\\t\\n,.!\"')\n",
    "tokenizer.fit_on_texts(data_train)\n",
    "\n",
    "# заменяем слова на их индексы в нашем словаре\n",
    "X_train = tokenizer.texts_to_sequences(data_train)\n",
    "X_test = tokenizer.texts_to_sequences(data_test)\n",
    "\n",
    "print(\"Предложение после замены слов на индексы:\\n\", X_train[0])\n",
    "\n",
    "# обрезаем каждое предложение приводим к нужной длинне\n",
    "X_train = pad_sequences(X_train, maxlen=MAX_SEQUENCE_LENGTH)\n",
    "X_test = pad_sequences(X_test, maxlen=MAX_SEQUENCE_LENGTH)\n",
    "\n",
    "print(\"Предложение после приведения к единой длинне:\\n\", X_train[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Задание 1\n",
    "\n",
    "Объект `Tokenizer` хранит в себе всю информацию про наш словарь. Нужно найти индекс слова \"super\" и сколько раз оно встречалось в нашей выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ЗАМЕНИТЕ 0 НА ПРАВИЛЬНЫЙ ОТВЕТ\n",
    "print(\"Индекс слова 'super' – {}.\".format(0))\n",
    "print(\"Слова 'super' встречалось {} раз.\".format(0))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details>\n",
    "  <summary>Здесь правильный ответ</summary>\n",
    "      <pre>\n",
    "            <code>\n",
    "              print(\"Индекс слова 'super' – {}.\".format(tokenizer.word_index['super']))\n",
    "              print(\"Слова 'super' встречалось {} раз.\".format(tokenizer.word_counts['super']))\n",
    "          </code>\n",
    "      </pre>\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# путь к файлу с эмбеддингами\n",
    "EMBEDDINGS_DIR = BASE_DIR + 'embeddings'\n",
    "EMBEDDINGS_FILE = 'glove.6B.50d.txt'\n",
    "\n",
    "EMBEDDING_DIM = 50\n",
    "\n",
    "# выберем только первые 10000 слов из нашего словаря\n",
    "first_10000 = {k: v for k, v in tokenizer.word_index.items() if v < 10000}\n",
    "\n",
    "# загрузим вектора эмбеддингов\n",
    "embeddings = {}\n",
    "with open(os.path.join(EMBEDDINGS_DIR, EMBEDDINGS_FILE)) as f:\n",
    "    for line in f:\n",
    "        values = line.split()\n",
    "        word = values[0]\n",
    "        coefs = np.asarray(values[1:], dtype='float32')\n",
    "        embeddings[word] = coefs\n",
    "    del values, word, coefs, line\n",
    "print(\"Количество слов в векторном представлении:\", len(embeddings))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Задание 2\n",
    "\n",
    "Посмотреть, сколько из наиболее встречаемых 10000 слов из нашего словаря нет в словаре эмбеддингов. Как можно данное к-во уменьшить?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ВАШ КОД"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details>\n",
    "  <summary>Здесь правильный ответ</summary>\n",
    "      <pre>\n",
    "            <code>\n",
    "            len(set(first_10000.keys()).difference(embeddings.keys()))\n",
    "            </code>\n",
    "            Добавить больше фильтров в аргумент `filters` объекта `Tokenizer`.\n",
    "      </pre>\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# подготовим матрицу эмбеддингов, где по строкам будут индексы слов\n",
    "\n",
    "embedding_matrix = np.zeros((tokenizer.num_words, EMBEDDING_DIM))\n",
    "for word, i in first_10000.items():\n",
    "    embedding_vector = embeddings.get(word)\n",
    "    if embedding_vector is not None:\n",
    "        embedding_matrix[i] = embedding_vector"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Рекуррентные нейронные сети (Recurrent neural networks, RNN)\n",
    "\n",
    "Рекуррентные нейронные сети помогают уловить/понять закономерность, которая зависит от времени или порядка. Например, когда мы пытаемся классифицировать какой-то эпизод из фильма, то нам важно знать что было пару эпизодов ранее, или чтобы понять смысл определенного слова, нам нужно знать контекст, который был до него.\n",
    "\n",
    "Простая рекуррентная нейронная сеть имеет следующее математическое представление:<br><br>\n",
    "$$\\large h_t = \\phi(Wx_t + Uh_{t-1})$$<br>\n",
    "$$\\large y = Vh_t$$\n",
    "\n",
    "Илюстрация к формуле:\n",
    "<img src=\"http://i.imgur.com/ifQrKRR.png\" alt=\"rnn\" style=\"width: 700px;\"/>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard\n",
    "from keras.layers import Activation, Dense, Embedding, SimpleRNN\n",
    "from keras.models import Sequential\n",
    "\n",
    "NAME = \"simple_rnn\"\n",
    "\n",
    "# инициализируем слой эмбеддингов\n",
    "\n",
    "embedding_layer = Embedding(tokenizer.num_words,\n",
    "                            EMBEDDING_DIM,\n",
    "                            weights=[embedding_matrix],\n",
    "                            input_length=MAX_SEQUENCE_LENGTH,\n",
    "                            trainable=False)\n",
    "                            \n",
    "model = Sequential()\n",
    "model.add(embedding_layer)\n",
    "model.add(SimpleRNN(100))\n",
    "model.add(Dense(1))\n",
    "model.add(Activation('sigmoid'))\n",
    "\n",
    "# инициализируем коллбеки\n",
    "# автоматическое построение кривых обучения\n",
    "callback_1 = TensorBoard(log_dir='./logs/logs_{}'.format(NAME), histogram_freq=0,\n",
    "                             write_graph=False, write_images=False)\n",
    "# остановка обучения при не увеличении точности в течении 5 эпох\n",
    "callback_2 = EarlyStopping(monitor='val_acc', min_delta=0, patience=5, verbose=0, mode='auto')\n",
    "# сохранение лучшей модели\n",
    "callback_3 = ModelCheckpoint(\"../models/model_{}.hdf5\".format(NAME), monitor='val_acc',\n",
    "                                 save_best_only=True, verbose=1)\n",
    "\n",
    "model.compile(loss='binary_crossentropy',\n",
    "              optimizer='adam',\n",
    "              metrics=['accuracy'])\n",
    "model.summary()\n",
    "#model.fit(X_train, labels_train, validation_data=[X_test, labels_test], \n",
    "#          batch_size=1024, epochs=100, callbacks=[callback_1, callback_2, callback_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Если натренировать данную модель, то точность на тренировочной выборке получится 77.13%, а на валидации – 75.60%.\n",
    "\n",
    "## Long short-term memory (LSTM)\n",
    "\n",
    "LSTM имеет ряд приемуществ над простой рекуррентной нейронной сетью. LSTM умеет хранить нужную информацию про определенный объект и не обращать внимание на неактуальную информацию. Например, сцена без упоминания главного героя не будет менять информацию про него и, наоборот, при упоминании она будет фокусироваться.\n",
    "\n",
    "- **Добавление механизма забывания.** Если эпизод заканчивается, например, модель должна забыть текущее местоположение, время суток и сбросить любую информацию о конкретной сцене. Однако если персонаж умирает в эпизоде, сеть должна должна продолжать помнить, что он больше не жив. Таким образом, мы хотим, чтобы модель изучила отдельный механизм забывания/запоминания: когда появляются новые входные данные, она должна знать, какие факты сохранить или выбросить.\n",
    "\n",
    "- **Добавление механизма сохранения.** Когда модель увидит новый эпизод, ей необходимо решить, стоит ли использовать и сохранять какую-либо информацию о ней. Ты увидел какой-то новый мем вконтакте, но зачем его помнить?\n",
    "\n",
    "- Поэтому когда приходит новый вход, модель сначала забывает долгосрочную информацию, которая, как она решает, больше не нужна. Затем она узнает, какую часть новых данных стоит использовать, и сохраняет ее в своей долгострочной памяти.\n",
    "\n",
    "- **Фокусировка с долгосрочной памяти в рабочую память.** Наконец, модель должна узнать, какие части ее долговременной памяти сейчас полезны. Например, возраст героя может быть полезной информацией для сохранения в долгосрочной перспективе (дети с большей вероятностью будут ползать, взрослые скорее всего будут работать), но, вероятно, не имеет значения, если он не находится в текущей сцене. Таким образом, вместо того чтобы использовать полную долгосрочную память все время, она узнает, на каких частях стоит сосредоточиться.\n",
    "\n",
    "То есть преимущество LSTM над RNN в том, что RNN может только перезаписывать память, а LSTM более гибкая в этом плане и может хранить долгосрочную информацию, фокусируясь на нужных ее частях.\n",
    "\n",
    "Рассмотрим теперь это все со стороны математики.\n",
    "\n",
    "Начнем с **долгосрочной памяти**. Во-первых, нам нужно знать, какие фрагменты долгосрочной памяти **продолжать помнить, а какие забывать**, поэтому нам нужно на основе нового входа и нашей рабочей памяти понять, какая часть долгосрочной должна сохраниться.\n",
    "\n",
    "<img src=\"http://colah.github.io/posts/2015-08-Understanding-LSTMs/img/LSTM3-focus-f.png\" alt=\"forget\" style=\"width: 600px;\"/>\n",
    "\n",
    "Таким образом, мы на выходе получаем значение от 0 до 1, где 0 – мы все забываем, 1 – все помним.\n",
    "\n",
    "Теперь необходимо решить, какую новую информацию запомнить и какую ее часть добавить в долгосрочную:\n",
    "<img src=\"http://colah.github.io/posts/2015-08-Understanding-LSTMs/img/LSTM3-focus-i.png\" alt=\"add\" style=\"width: 600px;\"/>\n",
    "\n",
    "Данный шаг состоит из двух частей: первая – это какую именно информацию мы хотим обновить ($ \\large i_t $); а вторая – кандидат на добавление в долгосрочную память ( $\\large \\tilde{C_t}$).\n",
    "\n",
    "Теперь мы должны обновить нашу долгосрочную память:\n",
    "\n",
    "<img src=\"http://colah.github.io/posts/2015-08-Understanding-LSTMs/img/LSTM3-focus-C.png\" alt=\"add\" style=\"width: 600px;\"/>\n",
    "\n",
    "После того как мы обновили нашу долгосрочную память, необходимо сфокусироваться на нужной информации для конкретного примера.\n",
    "\n",
    "<img src=\"http://colah.github.io/posts/2015-08-Understanding-LSTMs/img/LSTM3-focus-o.png\" alt=\"add\" style=\"width: 600px;\"/>\n",
    "\n",
    "Простыми словами, если мы фокусируемся на какой-то информации, то активация сигмоиды возвращает 1, а если нам какая-то информация сейчас не нужна, то мы возращаем 0.\n",
    "\n",
    "Давайте рассмотрим игрушечный пример для закрепления понимания. Будем тренироваться на покемонах :)\n",
    "\n",
    "Что \"думает\" полносвязная нейронная сеть:\n",
    "<img src=\"http://i.imgur.com/cOGzJxk.png\" alt=\"pokemon_nn\" style=\"heigh: 100px;\"/>\n",
    "\n",
    "Что \"думает\" простая рекуррентная сеть:\n",
    "<img src=\"http://i.imgur.com/PnWiSCf.png\" alt=\"pokemon_rnn\" style=\"heigh: 100px;\"/>\n",
    "\n",
    "Как видно из рисунка, рекуррентная сеть помнит, что случилось пару секунд назад, и может примерно понять, что стало причиной появления воды в следующем кадре.\n",
    "\n",
    "Что \"думает\" LSTM:\n",
    "<img src=\"http://i.imgur.com/EGZIUuc.png\" alt=\"pokemon_lstm\" style=\"heigh: 100px;\"/>\n",
    "\n",
    "LSTM вспоминает, что было в предыдущем эпизоде, а также подтягивает долгосрочную память и фокусируется только на нужной для конкретного эпизода информации.\n",
    "\n",
    "Источники:\n",
    "1. [Exploring LSTMs](http://blog.echen.me/2017/05/30/exploring-lstms/)\n",
    "2. [Understanding LSTM Networks](http://colah.github.io/posts/2015-08-Understanding-LSTMs/)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import LSTM\n",
    "\n",
    "# инициализируем слой эмбеддингов\n",
    "NAME = \"simple_lstm\"\n",
    "\n",
    "embedding_layer = Embedding(tokenizer.num_words,\n",
    "                            EMBEDDING_DIM,\n",
    "                            weights=[embedding_matrix],\n",
    "                            input_length=MAX_SEQUENCE_LENGTH,\n",
    "                            trainable=False)\n",
    "                            \n",
    "model = Sequential()\n",
    "model.add(embedding_layer)\n",
    "model.add(LSTM(100))\n",
    "model.add(Dense(1))\n",
    "model.add(Activation('sigmoid'))\n",
    "\n",
    "model.compile(loss='binary_crossentropy',\n",
    "              optimizer='adam',\n",
    "              metrics=['accuracy'])\n",
    "\n",
    "model.summary()\n",
    "#model.fit(X_train, labels_train, validation_data=[X_test, labels_test], \n",
    "#          batch_size=1024, epochs=100, callbacks=[callback_1, callback_2, callback_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Параметров в простой LSTM практически в 4 раза больше, математическое подверждение данному факту можно увидеть из формул выше.\n",
    "\n",
    "При замене простой RNN на LSTM точность на тренировочной выборке возросла до 82.49%, а на валидации – до 77.71%.\n",
    "\n",
    "### Задание 3\n",
    "Теперь давайте попробуем улучшить нашу простую модель LSTM, добавив в нее следующие модификации (к-во параметров не должно измениться):\n",
    "<details>\n",
    "  <summary>Здесь правильный ответ</summary>\n",
    "      <pre>\n",
    "          1. Дропауты (меньше переобучаемся на определенные слова).\n",
    "          2. Маскинг (данный параметр стоит добавить в инициализацию эмбеддингов, чтобы функция потерь не учитывала 0, когда наш отзыв меньше максимальной длинны).\n",
    "          3. Регуляризация (этот подход часто работает, но не в случае с LSTM, так как l1/l2 регуляризация для предотвращения взрывания градиентов, но ячейка LSTM построена таким образом, чтобы этого взрывания не было, так что использование  l1/l2 регуляризации нецелесообразно и ухудшает результаты).\n",
    "      </pre>\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import Dropout\n",
    "\n",
    "# инициализируем слой эмбеддингов\n",
    "NAME = \"modified_lstm\"\n",
    "\n",
    "embedding_layer = Embedding(tokenizer.num_words,\n",
    "                            EMBEDDING_DIM,\n",
    "                            weights=[embedding_matrix],\n",
    "                            input_length=MAX_SEQUENCE_LENGTH,\n",
    "                            trainable=False,\n",
    "                            mask_zero=True)\n",
    "                            \n",
    "model = Sequential()\n",
    "model.add(embedding_layer)\n",
    "model.add(Dropout(0.2))\n",
    "model.add(LSTM(100, dropout=0.1, recurrent_dropout=0.1))\n",
    "model.add(Dropout(0.2))\n",
    "model.add(Dense(1))\n",
    "model.add(Activation('sigmoid'))\n",
    "\n",
    "model.compile(loss='binary_crossentropy',\n",
    "              optimizer='adam',\n",
    "              metrics=['accuracy'])\n",
    "\n",
    "model.summary()\n",
    "#model.fit(X_train, labels_train, validation_data=[X_test, labels_test], \n",
    "#          batch_size=1024, epochs=100, callbacks=[callback_1, callback_2, callback_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данная модель показала себя ожидаемо лучше: на тренировочной выборке – 78.99%, на валидационной – 79.81%.<br>\n",
    "Варианты улучшения точности для LSTM модели в задаче сентимент анализа:\n",
    "- увеличение размерности эмбеддингов;\n",
    "- увеличение размерности выхода ячейки LSTM;\n",
    "- на больших данных работает увеличение к-ва слоев, bidirectional LSTM;\n",
    "- переход от маленького батча к большому в процессе обучения;\n",
    "- подбор гиперпараметров для дропаута, регуляризации и оптимизатора.\n",
    "\n",
    "Больше про реализацию различных видов LSTM можно посмотреть в [этом докладе]( https://github.com/udsclub/workshop/blob/master/notebooks/UDS-workshop-rnn.ipynb)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Сверточные нейронные сети\n",
    "\n",
    "Предобработка данных для сверточных нейронных сетей на словах ничем не отличается от предобработки для LSTM. Разве что на небольших данных лучше натренировать свои эмбеддинги, а не использовать заранее предобученные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import Conv1D, GlobalMaxPooling1D, MaxPooling1D\n",
    "\n",
    "# инициализируем слой эмбеддингов\n",
    "NAME = \"words_cnn\"\n",
    "\n",
    "EMBEDDING_DIM = 50\n",
    "\n",
    "# initialize model\n",
    "model = Sequential()\n",
    "model.add(Embedding(tokenizer.num_words, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH, trainable=True))\n",
    "model.add(Conv1D(activation=\"relu\", filters=100, kernel_size=4, padding=\"valid\"))\n",
    "model.add(Conv1D(activation=\"relu\", filters=100, kernel_size=4, padding=\"valid\"))\n",
    "model.add(Conv1D(activation=\"relu\", filters=100, kernel_size=4, padding=\"valid\"))\n",
    "model.add(GlobalMaxPooling1D())\n",
    "model.add(Dense(100, activation='relu'))\n",
    "model.add(Dropout(0.5))\n",
    "model.add(Dense(100, activation='relu'))\n",
    "model.add(Dense(1, activation='sigmoid'))\n",
    "\n",
    "model.compile(loss='binary_crossentropy',\n",
    "              optimizer='adam',\n",
    "              metrics=['accuracy'])\n",
    "\n",
    "model.summary()\n",
    "#model.fit(X_train, labels_train, validation_data=[X_test, labels_test], \n",
    "#          batch_size=1024, epochs=100, callbacks=[callback_1, callback_2, callback_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На наших данных результат сверточной нейронной сети на тренировочной выборке – 81.75, на валидации – 79.88% (причем всего за 2 итерации). И дальше идет сильное переобучение. На небольших данных сверточная сеть очень склонна к переобучению."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Нейронные сети на символах\n",
    "\n",
    "Существует два основных вида препроцессинга для нейронных сетей на символах:\n",
    "- аналогично словам, для символов нужно натренировать эмбеддинги;\n",
    "- символы представить в виде OHE эмбеддингов.\n",
    "\n",
    "Для начала разберем, какие символы включать. Включать символы можно любые, но по общей практике для английского языка используют 70 символов: буквы нижнего регистра, цифры и пунктуация. В зависимости от задачи или языка можно варьировать число символов.\n",
    "\n",
    "Рассмотрим пример с эмбеддингами с помощью небольшого задания.\n",
    "\n",
    "### Задание 4.\n",
    "Создать словарь из 70 символов и заменить в нашей выборке символы на их индексы в словаре. Подсказка: используйте библиотеку `string` и метод `from keras.preprocessing.sequence import pad_sequences`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import string\n",
    "\n",
    "from keras.preprocessing.sequence import pad_sequences\n",
    "\n",
    "\n",
    "def create_vocab_set():\n",
    "    \n",
    "    #1. ВАШ КОД\n",
    "    \n",
    "    return vocab, vocab_size\n",
    "\n",
    "def text2sequence(text, vocab):\n",
    "    temp = []\n",
    "    #2. ВАШ КОД\n",
    "    return temp\n",
    "\n",
    "vocab, vocab_size = create_vocab_set()\n",
    "\n",
    "X_train = text2sequence(data_train, vocab)\n",
    "X_test = text2sequence(data_test, vocab)\n",
    "\n",
    "#3. ВАШ КОД\n",
    "X_train = \n",
    "X_test = "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<details>\n",
    "  <summary>Здесь правильный ответ</summary>\n",
    "      <pre>\n",
    "            <code>\n",
    "              1. alphabet = (list(string.ascii_lowercase) + list(string.digits) + \n",
    "                              list(string.punctuation) + [' ', '\\n'])\n",
    "                 vocab_size = len(alphabet)\n",
    "                 vocab = {}\n",
    "                 for ix, t in enumerate(alphabet):\n",
    "                     vocab[t] = ix + 1\n",
    "              2. for review in text:\n",
    "                     temp.append([])\n",
    "                     for i in review:\n",
    "                         char = vocab.get(i,0)\n",
    "                         if char != 0:\n",
    "                             temp[-1].append(char)\n",
    "              3. X_train = pad_sequences(X_train, maxlen=MAX_SEQUENCE_LENGTH, value=0)\n",
    "                 X_val = pad_sequences(X_val, maxlen=MAX_SEQUENCE_LENGTH, value=0)\n",
    "            </code>\n",
    "      </pre>\n",
    "\n",
    "</details>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Эмбеддинги\n",
    "Такой же подход, как и для сверток на словах. Мы случайно инициализируем веса эмбеддингов и обновляем их во время обучения модели.\n",
    "#### Свертки\n",
    "Мы должны поэкспериментировать с ними и попытаться найти лучшую архитектуру для нашей задачи.\n",
    "#### GlobalMaxPooling1D\n",
    "В GlobalMaxPooling1D мы пытаемся выбрать самую важную фичу для каждого фильтра и передать ее на полносвязный слой.\n",
    "#### Полносвязный слой\n",
    "Это полносвязные слои с дропаутом между ними для уменьшения переобучения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import GlobalMaxPooling1D\n",
    "\n",
    "NAME = \"char_cnn_emb\"\n",
    "EMBEDDING_DIM = 50\n",
    "\n",
    "# инициализируем модель\n",
    "model = Sequential()\n",
    "model.add(Embedding(vocab_size+1, EMBEDDING_DIM, input_length=MAX_SEQUENCE_LENGTH, trainable=True))\n",
    "model.add(Conv1D(activation=\"relu\", filters=100, kernel_size=4, padding=\"valid\"))\n",
    "model.add(Conv1D(activation=\"relu\", filters=100, kernel_size=4, padding=\"valid\"))\n",
    "model.add(Conv1D(activation=\"relu\", filters=100, kernel_size=4, padding=\"valid\"))\n",
    "model.add(GlobalMaxPooling1D())\n",
    "model.add(Dense(100, activation='relu'))\n",
    "model.add(Dropout(0.5))\n",
    "model.add(Dense(100, activation='relu'))\n",
    "model.add(Dense(1, activation='sigmoid'))\n",
    "\n",
    "model.compile(loss='binary_crossentropy',\n",
    "              optimizer='adam',\n",
    "              metrics=['accuracy'])\n",
    "\n",
    "model.summary()\n",
    "#model.fit(X_train, labels_train, validation_data=[X_test, labels_test],\n",
    "#          batch_size=1024, epochs=100, callbacks=[callback_1, callback_2, callback_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Точность данной модели на валидации – 77.76%, что примерно равняется точности простой LSTM.\n",
    "\n",
    "Главный недостаток данного подхода в том, что мы никак не учитываем порядок слов (выполняются нелинейные преобразования, а потом выбираются лучшие признаки, и на них уже применяется несколько полносвязных слоев).\n",
    "\n",
    "Теперь рассмотрим второй подход к символьным моделям – **One-Hot-Encoding (OHE)**. Допустим у нас есть словарь из трех символов \"а\", \"б\", \"в\". OHE представление \"абва\" будет $$а – 0 0 \\\\ б – 1 0 \\\\ в – 0 1 \\\\ а – 0 0$$\n",
    "\n",
    "Для реализации воспользуемся дополнительными функциями из API Keras и TensorFlow."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "\n",
    "\n",
    "# ohe функция\n",
    "def ohe(x, sz):\n",
    "    return tf.to_float(tf.one_hot(x, sz, on_value=1, off_value=0, axis=-1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import Input, Lambda, MaxPooling1D\n",
    "from keras.models import Model\n",
    "\n",
    "NAME = \"char_cnn_ohe\"\n",
    "# инициализация входа\n",
    "in_sentence = Input(shape=(MAX_SEQUENCE_LENGTH,), dtype='int64')\n",
    "# Lambda слой для ohe преобразования\n",
    "embedded = Lambda(ohe, output_shape=lambda x: (x[0], x[1], vocab_size), arguments={\"sz\": vocab_size})(in_sentence)\n",
    "block = embedded\n",
    "# свертки с MaxPooling\n",
    "for i in range(3):\n",
    "    block = Conv1D(activation=\"relu\", filters=100, kernel_size=4, padding=\"valid\")(block)\n",
    "    if i == 0:\n",
    "        block = MaxPooling1D(pool_size=5)(block)\n",
    "# LSTM ячейка\n",
    "block = LSTM(128, dropout=0.1, recurrent_dropout=0.1)(block)\n",
    "block = Dense(100, activation='relu')(block)\n",
    "block = Dense(1, activation='sigmoid')(block)\n",
    "# собираем модель\n",
    "model = Model(inputs=in_sentence, outputs=block)\n",
    "model.compile(loss='binary_crossentropy',\n",
    "              optimizer='adam',\n",
    "              metrics=['accuracy'])\n",
    "\n",
    "\n",
    "model.summary()\n",
    "\n",
    "#model.fit(X_train, labels_train, validation_data=[X_test, labels_test],\n",
    "#          batch_size=1024, epochs=100, callbacks=[callback_1, callback_2, callback_3])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Точность на тренировочной выборке – 80.86%, на валидационной – 78.27%. \n",
    "\n",
    "У сверточных нейронных сетях есть несколько преимуществ перед LSTM:\n",
    "- не нужно хранить тысячи слов, а только небольшое к-во символов;\n",
    "- опечатки практически не влияют на точность модели (\"the bst film\" будет классифицирован как очень хороший, а LSTM просто проигнорирует данное слово).\n",
    "\n",
    "Эти подходы являются базовыми в применении глубокого обучения для задачи сентимент-анализа. Улучшать точность можно следующими модификациями:\n",
    "\n",
    "- размер эмбеддингов;\n",
    "- количество символов в словаре;\n",
    "- максимальная длина предложения;\n",
    "- модификация архитектуры сети;\n",
    "- микс сверток на словах и символах.\n",
    "\n",
    "Личные наблюдения:\n",
    "- если мало данных и отзывы короткие, то лучше использовать линейные методы, такие как логистическая регрессия или SVM (добавляя большое к-во n-грамм с сильной регуляризацией);\n",
    "- если мало данных, но отзывы длинные – однослойную LSTM;\n",
    "- много данных – стоит пробовать разные архитектуры сети LSTM, CNN, а так же их модификации.\n",
    "\n",
    "Лучшей моделью на этих данных оказалась наша модифицированная LSTM с предобученным w2v от гугла размерностью 300, словарем размера 50000 и большей размерности выхода LSTM ячейки. Точность на валидации составила больше 86.85%.\n",
    "Больше про решение задачи сентимент-анализа можно узнать из [этого](https://github.com/udsclub) GitHub-репозитория и [видео](https://www.youtube.com/watch?v=Zq-cw7YyHKA)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
